/**
 * Copyright 2008 Benjamin Possolo
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.javenue.util.process.impl;

import org.javenue.util.GenericsUtils;
import org.javenue.util.process.PersistenceException;
import org.javenue.util.process.Process;
import org.javenue.util.process.ProcessState;
import org.javenue.util.process.ProcessStateManagementException;
import org.javenue.util.process.ProcessStatePersister;
import org.javenue.util.process.Transactional;
import org.javenue.util.process.Transition;
import org.javenue.util.process.TransitionException;
import org.javenue.util.process.TransitionManager;
import org.javenue.util.process.TransitionWithResult;
import org.javenue.util.process.support.ProcessStateHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of the <tt>TransitionManager</tt> interface.
 * <p>
 * This class has annotated the <tt>TransitionManager</tt> interface's methods as <tt>@Transactional</tt>
 * so that AOP aspects can provide before/after-returning/after-throwing advice.
 * <p>
 * Hooks have been provided for a subclass to plug-in transaction management if AOP advice is not
 * offered to the public methods.
 * 
 * @author Benjamin Possolo
 * <p>Created on Jul 4, 2008
 */
public class TransitionManagerImpl implements TransitionManager {

	//-----------------------------------------------------
	//Class variables
	//-----------------------------------------------------
	/**
	 * A logger that subclasses can use.
	 */
	protected static final Logger log = LoggerFactory.getLogger(TransitionManagerImpl.class);
	
	//-----------------------------------------------------
	//Member variables
	//-----------------------------------------------------
	/**
	 * The ProcessStatePersister that is used to load and persist the process state instances.
	 */
	private ProcessStatePersister processStatePersister; //injected
	
	//-----------------------------------------------------
	//Public methods
	//-----------------------------------------------------
	
	/**
	 * Sets the ProcessStatePersister that is used to load and persist the process state instances
	 * @param processStatePersister the ProcessStatePersister to set
	 */
	public void setProcessStatePersister(ProcessStatePersister processStatePersister) {
		this.processStatePersister = processStatePersister;
	}
	
	//-----------------------------------------------------
	//Implementation of TransitionManager
	//-----------------------------------------------------
	
	/**
     * @see TransitionManager#execute(Process, Transition)
     */
	@Transactional
	public void execute(Process<? extends ProcessState> process, Transition transition)
			throws TransitionException, ProcessStateManagementException {
		
		invokeBeforeTransition();
			
		if( log.isDebugEnabled() )
			log.debug("Executing transition with process state management for process ["
				+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
		
		loadProcessState(process);
			
		TransitionException error = null;
			
		try{
			transition.doTransition();
		}
		catch(Throwable t){
			error = new TransitionException("User code in Transition experienced an error", t);
		}
		
		if( error == null ){ //if user code executed properly
			if( log.isDebugEnabled() )
				log.debug("Transition executed succesfully for process ["
					+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
			
			persistProcessState(process);
			
			invokeAfterTransitionReturns(null);
		}
		else{
			if( log.isDebugEnabled() ){
				log.debug("An error occured while executing transiton for process ["
					+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
				log.debug("Process' state will not be persisted");
			}
			invokeAfterTransitionThrows(error);
			throw error;
		}
	}

	/**
     * @see TransitionManager#execute(Process, TransitionWithResult)
     */
	@Transactional
	public Object execute(Process<? extends ProcessState> process, TransitionWithResult transition)
			throws TransitionException, ProcessStateManagementException {
		
		invokeBeforeTransition();
		
		if( log.isDebugEnabled() )
			log.debug("Executing transition with process state management for process ["
				+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
		
		loadProcessState(process);
		
		Object result = null;
		TransitionException error = null;
		
		try{
			result = transition.doTransition();
		}
		catch(Throwable t){
			error = new TransitionException("User code in Transition experienced an error", t);
		}
		
		if( error == null ){
			if( log.isDebugEnabled() )
				log.debug("Transition executed succesfully for process ["
					+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
			
			persistProcessState(process);
			
			invokeAfterTransitionReturns(result);
			return result;
		}
		else{
			if( log.isDebugEnabled() ){
				log.debug("An error occured while executing transiton for process ["
					+ process.getClass().getName() + "] with ID [" + process.getId() + "]");
				log.debug("Process' state will not be persisted");
			}
			invokeAfterTransitionThrows(error);
			throw error;
		}
	}

	/**
     * @see TransitionManager#executeWithoutStateManagement(Transition)
     */
	@Transactional
	public void executeWithoutStateManagement(Transition transition)
			throws TransitionException {
		
		invokeBeforeTransition();
		
		log.debug("Executing transition");
		
		try{
			transition.doTransition();
			log.debug("Transition completed successfully");
			
			invokeAfterTransitionReturns(null);
		}
		catch(Throwable t){
			TransitionException te = new TransitionException("User code in Transition experienced an error", t);
			invokeAfterTransitionThrows(te);
			throw te;
		}
	}

	/**
     * @see TransitionManager#executeWithoutStateManagement(TransitionWithResult)
     */
	@Transactional
	public Object executeWithoutStateManagement(TransitionWithResult transition)
			throws TransitionException {
		
		invokeBeforeTransition();
		
		log.debug("Executing transition");
		
		try{
			Object result = transition.doTransition();
			log.debug("Transition completed successfully");
			
			invokeAfterTransitionReturns(result);
			
			return result;
		}
		catch(Throwable t){
			TransitionException te = new TransitionException("User code in Transition experienced an error", t);
			invokeAfterTransitionThrows(te);
			throw te;
		}
	}
	
	//-----------------------------------------------------
	//Protected methods
	//-----------------------------------------------------
	
	/**
	 * Hook for subclasses to add custom behaviour before a <tt>Transition</tt> or <tt>TransitionWithResult</tt> is executed.
	 * If transactional advice is not being offered to this class via an aspect, then it is necessary for a transaction to be started
	 * within this method which is invoked before a transition's <code>doTransition()</code> is invoked.
	 */
	protected void beforeTransition(){
		
		log.debug("BeforeTransition");
	}
	
	/**
	 * Hook for subclasses to add custom behaviour after a <tt>Transition</tt> or <tt>TransitionWithResult</tt> has successfully executed.
	 * If transactional advice is not being offered to this class via an aspect, then it is necessary for a transaction to be committed
	 * within this method which is invoked after a transition's <code>doTransition()</code> has completed.
	 */
	protected void afterTransitionReturns(final Object transitionResult){
		
		log.debug("AfterTransitionReturns");
	}
	
	/**
	 * Hook for subclasses to add custom behaviour after a <tt>Transition</tt> or <tt>TransitionWithResult</tt> throws an exception.
	 * If transactional advice is not being offered to this class via an aspect, then it is necessary for a transaction to be rolled-back
	 * within this method which is invoked after a transition's <code>doTransition()</code> throws an exception.
	 */
	protected void afterTransitionThrows(final TransitionException exception){
		
		log.debug("AfterTransitionThrows");
	}
	
	//-----------------------------------------------------
	//Private methods
	//-----------------------------------------------------
	
	/**
	 * Loads a <tt>ProcessState</tt> instance from the persistence store.
	 * <p>
	 * The <tt>ProcessState</tt> instance to retrieve is determined by the <tt>Process</tt>
	 * argument's ID value. If a corresponding entry does not exist in the persistence store
	 * (for example, if this is the first time that the process is executing a transition),
	 * then a new instance of the respective type is instantiated, saved and then returned.
	 * <p>
	 * The type of <tt>ProcessState</tt> to instantiate/load is determined by the
	 * generic type argument that was supplied when the concrete <tt>Process</tt> was defined. 
	 * 
	 * @param process the <tt>Process</tt> whose <tt>ProcessState</tt> is to be loaded
	 * @throws ProcessStateManagementException if an error occurs creating the <tt>ProcessState</tt>
	 * instance or while saving it to the persistence store
	 * @throws IllegalStateException if the <code>processStatePersister</code> property is null or if the process's ID value is null
	 */
	@SuppressWarnings("unchecked")
	private void loadProcessState(Process process) throws ProcessStateManagementException {
		
		if( processStatePersister == null )
			throw new IllegalStateException("processStatePersister is null");
		
		if( process.getId() == null )
			throw new IllegalStateException("process ID value is null");
		
		Class clazz = GenericsUtils.getTypeArguments(Process.class, process.getClass()).get(0);
		
		if( log.isDebugEnabled() )
			log.debug("Loading process state for [" 
				+ process.getClass().getName() + "] with ID [" 
				+ process.getId() + "]");
		
		try{
			ProcessState ps = processStatePersister.getProcessState(clazz, process.getId());
			
			if( ps == null ){
				if( log.isDebugEnabled() )
					log.debug("Creating new instance of [" + clazz.getName() + "]");
				ps = (ProcessState)clazz.newInstance();
				ProcessStateHelper.setField(ps, "id", process.getId());
				processStatePersister.saveNewInstance(ps);
			}
			
			ProcessStateHelper.setProcessState(process, ps);
		}
		catch(IllegalAccessException e){
			throw new ProcessStateManagementException("Unable to create instace of class [" + clazz.getCanonicalName() + "]", e);
		}
		catch(InstantiationException e){
			throw new ProcessStateManagementException("Unable to create instace of class [" + clazz.getCanonicalName() + "]", e);
		}
		catch(NoSuchFieldException e){
			throw new ProcessStateManagementException("Unable to set ID value in ProcessState instance");
		}
		catch(PersistenceException e){
			throw new ProcessStateManagementException("Error loading process state", e);
		}
	}
	
	/**
	 * Persists a <tt>Process</tt>'s <tt>ProcessState</tt> instance by calling the 
	 * <tt>ProcessStatePersister</tt>'s <code>persistChanges(ProcessState)</code> method. 
	 * <p>
	 * This method assumes that a <tt>ProcessState</tt> with the correspond ID already 
	 * exists in the persistence store.
	 * 
	 * @param process the <tt>Process</tt> whose <tt>ProcessState</tt> is to be persisted
	 * @throws ProcessStateManagementException if an error occurs while persisting the changes
	 * @throws IllegalStateException if the <code>processStatePersister</code> property is null
	 */
	private void persistProcessState(Process<? extends ProcessState> process) throws ProcessStateManagementException {
		
		if( processStatePersister == null )
			throw new IllegalStateException("processStatePersister is null");
		
		try{
			if( log.isDebugEnabled() )
				log.debug("Persisting process state for [" 
						+ process.getClass().getName() + "] with ID [" 
						+ process.getId() + "]");
			
			ProcessState processState = ProcessStateHelper.getProcessState(process);
			
			processStatePersister.persistChanges(processState);
		}
		catch(PersistenceException e){
			throw new ProcessStateManagementException("Error persisting process state", e);
		}
	}
	
	
	/**
	 * Invokes the subclass hook method: <code>beforeTransition()</code>.
	 * 
	 * @throws ProcessStateManagementException if the overridden method throws an exception when invoked
	 */
	private void invokeBeforeTransition() throws ProcessStateManagementException {
		
		try{
			beforeTransition();
		}
		catch(RuntimeException e){
			throw new ProcessStateManagementException("overridden method 'beforeTransition()' failed", e);
		}
	}
	
	/**
	 * Invokes the subclass hook method: <code>afterTransitionReturns()</code>.
	 * 
	 * @param transitionResult the result of a <tt>TransitionWithResult</tt> or null if a <tt>Transition</tt> was executed
	 * @throws ProcessStateManagementException  if the overridden method throws an exception when invoked
	 */
	private void invokeAfterTransitionReturns(final Object transitionResult) throws ProcessStateManagementException {
		
		try{
			afterTransitionReturns(transitionResult);
		}
		catch(RuntimeException e){
			throw new ProcessStateManagementException("overridden method 'afterTransitionReturns()' failed", e);
		}
	}
	
	/**
	 * Invokes the subclass hook method: <code>afterTransitionThrows()</code>.
	 * 
	 * @param exception the exception that caused the <tt>Transition</tt> or <tt>TransitionWithResult</tt> to fail
	 * @throws ProcessStateManagementException  if the overridden method throws an exception when invoked
	 */
	private void invokeAfterTransitionThrows(final TransitionException exception) throws ProcessStateManagementException {
		
		try{
			afterTransitionThrows(exception);
		}
		catch(RuntimeException e){
			throw new ProcessStateManagementException("overridden method 'afterTransitionThrows()' failed", e);
		}
	}
}
